王绵杰的个人博客

iOS 源码分析系列---MBProgressHUD!

HUDMode

首先来看MBProgressHUD的几种显示模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef NS_ENUM(NSInteger, MBProgressHUDMode) {
/// 默认模式,无进度,只能转
MBProgressHUDModeIndeterminate,
/// 扇形图显示进度
MBProgressHUDModeDeterminate,
/// 进度条显示状态
MBProgressHUDModeDeterminateHorizontalBar,
/// 圆环显示
MBProgressHUDModeAnnularDeterminate,
/// 自定义视图显示
MBProgressHUDModeCustomView,
/// 仅文本显示
MBProgressHUDModeText
};

核心API

属性

1
2
3
4
5
6
7
8
/// show系列函数触发到显示HUD的时间段
@property (assign, nonatomic) NSTimeInterval graceTime;
/// HUD最短显示时间
@property (assign, nonatomic) NSTimeInterval minShowTime;
/// HUD的显示模式
@property (assign, nonatomic) MBProgressHUDMode mode;
/// HUD的显示动画类型
@property (assign, nonatomic) MBProgressHUDAnimation animationType UI_APPEARANCE_SELECTOR;

显示逻辑

MBProgressHUD作者把方法的定义放在最上面,而很多人是把属性放在方法上面,这只是习惯问题,无伤大雅:

1
2
3
4
5
6
7
8
/// 在某View上显示HUD,该方法在显示之前会移除之前的HUD以达到好的性能和体验
+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated {
MBProgressHUD *hud = [[self alloc] initWithView:view]; // 调用 [self initWithFrame:view.bounds]:根据传进来的view的frame来设定自己的frame
hud.removeFromSuperViewOnHide = YES; // 设置hud在hide状态时,移除
[view addSubview:hud]; // 在view上添加实例
[hud showAnimated:animated];
return hud;
}

调用showAnimated,这里首先进行线程安全判断,因为对于 UIView 的处理必须在主线程中, 所以在这里要先用 [NSThread isMainThread] 来确认当前前程为主线程,而 MBMainThreadAssert()就是一个主线程安全判断的宏定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
- (void)showAnimated:(BOOL)animated {
MBMainThreadAssert();
[self.minShowTimer invalidate]; // 取消当前的minShowTimer
self.useAnimation = animated;
self.finished = NO; // 设置完成标记
// If the grace time is set, postpone the HUD display
// 如果设定了graceTime,就要推迟HUD的显示,这里graceTime的意义是用来推迟HUD的显示。如果设定了graceTime,那么HUD会在 show 方法触发后的graceTime时间后显示。它的意义是:如果任务完成所消耗的时间非常短并且短于graceTime,则HUD就不会出现了,避免HUD一闪而过的差体验。
if (self.graceTime > 0.0) {
NSTimer *timer = [NSTimer timerWithTimeInterval:self.graceTime target:self selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.graceTimer = timer;
}
// ... otherwise show the HUD immediately
else {
[self showUsingAnimation:self.useAnimation];
}
}
// self.graceTime的触发方法,当未结束时,动画显示hud
- (void)handleGraceTimer:(NSTimer *)theTimer {
// Show the HUD only if the task is still running
if (!self.hasFinished) {
[self showUsingAnimation:self.useAnimation];
}
}
// 这是一个显示的核心方法,所有的显示hud的方法最终都会调用这个方法
- (void)showUsingAnimation:(BOOL)animated {
// Cancel any previous animations
// 移除已有hud的动画
[self.bezelView.layer removeAllAnimations];
[self.backgroundView.layer removeAllAnimations];
// Cancel any scheduled hideDelayed: calls
// 取消延迟hide 的timer
[self.hideDelayTimer invalidate];
// 记录开始的时间
self.showStarted = [NSDate date];
// 将试图的可视度设为1,之前在视图设置里已默认设置为0
self.alpha = 1.f;
// Needed in case we hide and re-show with the same NSProgress object attached.
[self setNSProgressDisplayLinkEnabled:YES]; // 设置progress显示监听
if (animated) {
[self animateIn:YES withType:self.animationType completion:NULL];
} else {
// 方法弃用警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
self.bezelView.alpha = self.opacity;
#pragma clang diagnostic pop
self.backgroundView.alpha = 1.f;
}
}

这里需要说的一点,showUsingAnimation方法设置了NSProgress的监听方法,来防止程序多次显示同一Hud时的progress值错误,这里使用CADisplayLink 来刷新progress的变化。那么会有人问了为什么没有使用NSTimer和KVO呢?因为CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高,并且不需要关心屏幕的刷新频率,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染;而NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期;如果使用kvo机制来监听的话,因为动画刷新频率比较快,可能会非常消耗主线程,拖重性能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled {
// We're using CADisplayLink, because NSProgress can change very quickly and observing it may starve the main thread,
// so we're refreshing the progress only every frame draw
if (enabled && self.progressObject) {
// Only create if not already active.
if (!self.progressObjectDisplayLink) {
self.progressObjectDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateProgressFromProgressObject)];
}
} else {
self.progressObjectDisplayLink = nil;
}
}
- (void)updateProgressFromProgressObject {
self.progress = self.progressObject.fractionCompleted;
}

隐藏逻辑

+ hideHUDForView:animated:方法的实现和 + showHUDAddedTo:animated:差不多,而+ HUDForView:方法会返回对应 view 最上层的 MBProgressHUD 的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated {
MBProgressHUD *hud = [self HUDForView:view];
if (hud != nil) {
hud.removeFromSuperViewOnHide = YES;
[hud hideAnimated:animated];
return YES;
}
return NO;
}
+ (MBProgressHUD *)HUDForView:(UIView *)view {
NSEnumerator *subviewsEnum = [view.subviews reverseObjectEnumerator];
for (UIView *subview in subviewsEnum) {
if ([subview isKindOfClass:self]) {
return (MBProgressHUD *)subview;
}
}
return nil;
}

然后调用的- hideAnimated: 方法和 - hideUsingAnimation:方法,来实现hud的隐藏,同样- hideUsingAnimation:方法也是每个隐藏方法的必调核心方法,而只有在 HUD 隐藏之后 - done 负责隐藏执行completionBlockdelegate的回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
- (void)hideAnimated:(BOOL)animated {
MBMainThreadAssert();
[self.graceTimer invalidate];
self.useAnimation = animated;
self.finished = YES;
// If the minShow time is set, calculate how long the HUD was shown,
// and postpone the hiding operation if necessary
if (self.minShowTime > 0.0 && self.showStarted) {
NSTimeInterval interv = [[NSDate date] timeIntervalSinceDate:self.showStarted];
if (interv < self.minShowTime) {
NSTimer *timer = [NSTimer timerWithTimeInterval:(self.minShowTime - interv) target:self selector:@selector(handleMinShowTimer:) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.minShowTimer = timer;
return;
}
}
// ... otherwise hide the HUD immediately
[self hideUsingAnimation:self.useAnimation];
}
- (void)hideUsingAnimation:(BOOL)animated {
if (animated && self.showStarted) {
self.showStarted = nil;
[self animateIn:NO withType:self.animationType completion:^(BOOL finished) {
[self done];
}];
} else {
self.showStarted = nil;
self.bezelView.alpha = 0.f;
self.backgroundView.alpha = 1.f;
[self done];
}
}
- (void)done {
// Cancel any scheduled hideDelayed: calls
[self.hideDelayTimer invalidate];
[self setNSProgressDisplayLinkEnabled:NO];
if (self.hasFinished) {
self.alpha = 0.0f;
if (self.removeFromSuperViewOnHide) {
[self removeFromSuperview];
}
}
MBProgressHUDCompletionBlock completionBlock = self.completionBlock;
if (completionBlock) {
completionBlock();
}
id<MBProgressHUDDelegate> delegate = self.delegate;
if ([delegate respondsToSelector:@selector(hudWasHidden:)]) {
[delegate performSelector:@selector(hudWasHidden:) withObject:self];
}
}

通过观察hud的显示和隐藏逻辑不难看出,无论是 show 方法,还是 hide 方法,在设定animated属性为YES的前提下,最终都会走到 animateIn: withType: completion: 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
- (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion {
// Automatically determine the correct zoom animation type
if (type == MBProgressHUDAnimationZoom) {
type = animatingIn ? MBProgressHUDAnimationZoomIn : MBProgressHUDAnimationZoomOut;
}
// 设置x,y的缩放倍数
CGAffineTransform small = CGAffineTransformMakeScale(0.5f, 0.5f);
CGAffineTransform large = CGAffineTransformMakeScale(1.5f, 1.5f);
// Set starting state
// 初始化hud状态
UIView *bezelView = self.bezelView;
if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomIn) {
bezelView.transform = small;
} else if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomOut) {
bezelView.transform = large;
}
// Perform animations
// 创建动画
dispatch_block_t animations = ^{
if (animatingIn) {
bezelView.transform = CGAffineTransformIdentity;
} else if (!animatingIn && type == MBProgressHUDAnimationZoomIn) {
bezelView.transform = large;
} else if (!animatingIn && type == MBProgressHUDAnimationZoomOut) {
bezelView.transform = small;
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
bezelView.alpha = animatingIn ? self.opacity : 0.f;
#pragma clang diagnostic pop
// 这里实现show和hide方法的区分,animationIn yes时为show方法,no时为hide方法,从而实现显示与隐藏
self.backgroundView.alpha = animatingIn ? 1.f : 0.f;
};
// Spring animations are nicer, but only available on iOS 7+
// 兼容性在7.0以上
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 || TARGET_OS_TV
if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_7_0) {
[UIView animateWithDuration:0.3 delay:0. usingSpringWithDamping:1.f initialSpringVelocity:0.f options:UIViewAnimationOptionBeginFromCurrentState animations:animations completion:completion];
return;
}
#endif
[UIView animateWithDuration:0.3 delay:0. options:UIViewAnimationOptionBeginFromCurrentState animations:animations completion:completion];
}

此框架可取之处:

1、代码的质量非常高,方法的抽象程度高,但是不繁琐,暴露对外的API最终走向同一个私有方法,这种设计思想,使得框架维护起来十分方便快捷。

2、设计显示上加上的graceTimer、minShowTimer、hideDelayTimer的概念,使得hud的显示与隐藏有了缓冲的时间,可以提高稳定性和定制性。